還記得 30 天前,我們從一個簡單的測試開始嗎?
test('myFirstTest', () => {
  expect(1 + 1).toBe(2);
});
今天,我們不僅完成了完整的 Todo 應用,更重要的是建立了 TDD 思維。這趟旅程的終點,正是你成為更好開發者的起點。
今天我們要完成最後一塊拼圖:
第一週 - 測試基礎 ✅
├── Day 01-03: 環境設置與 TDD 基礎
├── Day 04-06: 測試結構與組織
└── Day 07-10: 測試替身與覆蓋率
第二週 - 進階應用 ✅  
├── Day 11-14: 實戰練習基礎
└── Day 15-17: 進階實作與總結
第三週 - 框架測試 ✅
├── Day 18-21: React Testing Library
└── Day 22-24: 進階測試技巧
第四週+ - 整合實戰 ✅
├── Day 25-27: MSW 與 E2E 測試
├── Day 28-29: CI/CD 與監控
└── Day 30: 部署與總結 🎯 您在這裡!
在部署之前,讓我們確認所有的準備工作:
// 建立 tests/day30/deployment-readiness.test.ts
import { describe, test, expect } from 'vitest';
describe('Deployment Readiness', () => {
  test('allUnitTestsPass', async () => {
    const { results } = await import('../test-results.json');
    expect(results.numFailedTests).toBe(0);
    expect(results.numPassedTests).toBeGreaterThan(50);
  });
  test('coverageMeetsRequirements', async () => {
    const coverage = await import('../coverage/coverage-summary.json');
    expect(coverage.total.lines.pct).toBeGreaterThan(80);
  });
  test('bundleSizeIsAcceptable', async () => {
    const stats = await import('../dist/stats.json');
    const mainBundleSize = stats.assets.find(
      (asset: any) => asset.name.includes('main')
    )?.size || 0;
    expect(mainBundleSize).toBeLessThan(200000);
  });
  test('environmentVariablesAreSet', () => {
    expect(process.env.VITE_API_URL).toBeDefined();
    expect(process.env.VITE_APP_VERSION).toBeDefined();
  });
});
// 建立 vite.config.prod.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
  plugins: [react()],
  build: {
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom', 'react-router-dom'],
          utils: ['axios', 'date-fns'],
        },
      },
    },
    sourcemap: false,
  },
});
// 建立 vercel.json
{
  "buildCommand": "npm run build",
  "framework": "vite",
  "outputDirectory": "dist",
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ],
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "no-cache" }
      ]
    },
    {
      "source": "/assets/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000" }
      ]
    }
  ]
}
// 建立 tests/e2e/health-check.spec.ts
import { test, expect } from '@playwright/test';
const PRODUCTION_URL = 'https://todo-app.vercel.app';
test.describe('Production Health Check', () => {
  test('applicationLoadsSuccessfully', async ({ page }) => {
    const response = await page.goto(PRODUCTION_URL);
    expect(response?.status()).toBe(200);
    await expect(page.locator('h1')).toContainText('Todo App');
  });
  test('canPerformCrudOperations', async ({ page }) => {
    await page.goto(PRODUCTION_URL);
    
    // Create
    await page.fill('[data-testid="todo-input"]', 'Test todo');
    await page.click('[data-testid="add-todo"]');
    await expect(page.locator('[data-testid="todo-item"]'))
      .toContainText('Test todo');
    
    // Update
    await page.click('[data-testid="edit-todo-0"]');
    await page.fill('[data-testid="edit-input"]', 'Updated');
    await page.click('[data-testid="save-edit"]');
    
    // Delete
    await page.click('[data-testid="delete-todo-0"]');
    await expect(page.locator('[data-testid="todo-item"]'))
      .not.toBeVisible();
  });
  test('performanceMetricsAreAcceptable', async ({ page }) => {
    await page.goto(PRODUCTION_URL);
    
    const metrics = await page.evaluate(() => {
      const nav = performance.getEntriesByType('navigation')[0] 
        as PerformanceNavigationTiming;
      return {
        domContentLoaded: nav.domContentLoadedEventEnd,
        loadComplete: nav.loadEventEnd,
      };
    });
    
    expect(metrics.domContentLoaded).toBeLessThan(3000);
    expect(metrics.loadComplete).toBeLessThan(5000);
  });
});
// 建立 src/utils/monitoring.ts
export class ProductionMonitor {
  private static instance: ProductionMonitor;
  private metrics = new Map<string, any>();
  static getInstance(): ProductionMonitor {
    if (!this.instance) {
      this.instance = new ProductionMonitor();
    }
    return this.instance;
  }
  trackPerformance() {
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver((list) => {
        list.getEntries().forEach((entry) => {
          this.metrics.set(`perf_${entry.name}`, {
            duration: entry.duration,
            timestamp: Date.now(),
          });
        });
      });
      observer.observe({ entryTypes: ['measure', 'navigation'] });
    }
  }
  trackErrors() {
    window.addEventListener('error', (event) => {
      console.error('Production Error:', {
        message: event.message,
        source: event.filename,
        line: event.lineno,
      });
    });
  }
}
// 建立 src/main.tsx
import { ProductionMonitor } from './utils/monitoring';
if (import.meta.env.PROD) {
  const monitor = ProductionMonitor.getInstance();
  monitor.trackPerformance();
  monitor.trackErrors();
}
// Day 1 的我們
test('simpleAddition', () => {
  expect(1 + 1).toBe(2);
});
// Day 30 的我們  
describe('Todo Application', () => {
  beforeAll(async () => {
    await setupTestDatabase();
  });
  test('completeUserJourney', async () => {
    const user = userEvent.setup();
    render(<App />);
    
    // 登入流程
    await user.type(screen.getByLabelText('Email'), 'test@example.com');
    await user.click(screen.getByRole('button', { name: 'Login' }));
    
    // 創建 Todo
    await waitFor(() => 
      expect(screen.getByText('My Todos')).toBeInTheDocument()
    );
    await user.type(screen.getByPlaceholderText('What needs to be done?'), 
      'Learn TDD');
    await user.click(screen.getByRole('button', { name: 'Add' }));
    
    // 驗證結果
    expect(await screen.findByText('Learn TDD')).toBeInTheDocument();
  });
});
Day 1-10: 建立測試基礎
Day 11-17: 實戰練習
Day 18-27: 框架實戰
Day 28-30: 專業工程實踐
在真實專案中應用 TDD
持續優化測試策略
分享你的經驗
TDD 不只是一種測試方法,更是一種思維方式。這 30 天,我們學會了:
記住:好的測試不是為了通過,而是為了失敗時能告訴你原因。
30 天前,我們從零開始學習 TDD。今天,我們不僅完成了完整的專案,更重要的是建立了測試優先的開發思維。
這不是結束,而是開始。TDD 的精髓不在於技術,而在於持續追求更好的程式碼品質。每一個測試都是對未來的承諾,每一次重構都是對品質的堅持。
記住:
感謝你完成這 30 天的挑戰!現在,帶著 TDD 的力量,去創造更棒的軟體吧!
恭喜完成 30 天 TDD 實戰挑戰!
願測試與你同在,願程式碼永遠優雅!
感謝閱讀 30 天系列文章,歡迎持續關注 TDD 實踐之路!